package keeper

import (
	"context"
	"fmt"

	st "cosmossdk.io/api/cosmos/staking/v1beta1"
	"cosmossdk.io/core/comet"
	"cosmossdk.io/core/event"
	"cosmossdk.io/x/slashing/types"

	cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
	sdk "github.com/cosmos/cosmos-sdk/types"
)

// HandleValidatorSignature handles a validator signature, must be called once per validator for each block.
func (k Keeper) HandleValidatorSignature(ctx context.Context, addr cryptotypes.Address, power int64, signed comet.BlockIDFlag) error {
	params, err := k.Params.Get(ctx)
	if err != nil {
		return err
	}
	return k.HandleValidatorSignatureWithParams(ctx, params, addr, power, signed)
}

// HandleValidatorSignature handles a validator signature with the provided slashing module params.
func (k Keeper) HandleValidatorSignatureWithParams(ctx context.Context, params types.Params, addr cryptotypes.Address, power int64, signed comet.BlockIDFlag) error {
	height := k.HeaderService.HeaderInfo(ctx).Height

	// fetch the validator public key
	consAddr := sdk.ConsAddress(addr)

	val, err := k.sk.ValidatorByConsAddr(ctx, consAddr)
	if err != nil {
		return err
	}

	// don't update missed blocks when validator's jailed
	if val.IsJailed() {
		return nil
	}

	// read the cons address again because validator may've rotated its key
	valConsAddr, err := val.GetConsAddr()
	if err != nil {
		return err
	}

	consAddr = sdk.ConsAddress(valConsAddr)

	// fetch signing info
	signInfo, err := k.ValidatorSigningInfo.Get(ctx, consAddr)
	if err != nil {
		return err
	}

	signedBlocksWindow := params.SignedBlocksWindow

	// Compute the relative index, so we count the blocks the validator *should*
	// have signed. We will also use the 0-value default signing info if not present.
	// The index is in the range [0, SignedBlocksWindow)
	// and is used to see if a validator signed a block at the given height, which
	// is represented by a bit in the bitmap.
	// The validator start height should get mapped to index 0, so we computed index as:
	// (height - startHeight) % signedBlocksWindow
	//
	// NOTE: There is subtle different behavior between genesis validators and non-genesis validators.
	// A genesis validator will start at index 0, whereas a non-genesis validator's startHeight will be the block
	// they bonded on, but the first block they vote on will be one later. (And thus their first vote is at index 1)
	index := (height - signInfo.StartHeight) % signedBlocksWindow
	if signInfo.StartHeight > height {
		return fmt.Errorf("invalid state, the validator %v has start height %d , which is greater than the current height %d (as parsed from the header)",
			signInfo.Address, signInfo.StartHeight, height)
	}

	// determine if the validator signed the previous block
	previous, err := k.GetMissedBlockBitmapValue(ctx, consAddr, index)
	if err != nil {
		return fmt.Errorf("failed to get the validator's bitmap value: %w", err)
	}

	modifiedSignInfo := false
	missed := signed == comet.BlockIDFlagAbsent
	switch {
	case !previous && missed:
		// Bitmap value has changed from not missed to missed, so we flip the bit
		// and increment the counter.
		if err := k.SetMissedBlockBitmapValue(ctx, consAddr, index, true); err != nil {
			return err
		}

		signInfo.MissedBlocksCounter++
		modifiedSignInfo = true

	case previous && !missed:
		// Bitmap value has changed from missed to not missed, so we flip the bit
		// and decrement the counter.
		if err := k.SetMissedBlockBitmapValue(ctx, consAddr, index, false); err != nil {
			return err
		}

		signInfo.MissedBlocksCounter--
		modifiedSignInfo = true

	default:
		// bitmap value at this index has not changed, no need to update counter
	}

	minSignedPerWindow := params.MinSignedPerWindowInt()

	consStr, err := k.sk.ConsensusAddressCodec().BytesToString(consAddr)
	if err != nil {
		return err
	}

	if missed {
		if err := k.EventService.EventManager(ctx).EmitKV(
			types.EventTypeLiveness,
			event.NewAttribute(types.AttributeKeyAddress, consStr),
			event.NewAttribute(types.AttributeKeyMissedBlocks, fmt.Sprintf("%d", signInfo.MissedBlocksCounter)),
			event.NewAttribute(types.AttributeKeyHeight, fmt.Sprintf("%d", height)),
		); err != nil {
			return err
		}

		k.Logger.Debug(
			"absent validator",
			"height", height,
			"validator", consStr,
			"missed", signInfo.MissedBlocksCounter,
			"threshold", minSignedPerWindow,
		)
	}

	minHeight := signInfo.StartHeight + signedBlocksWindow
	maxMissed := signedBlocksWindow - minSignedPerWindow

	// if we are past the minimum height and the validator has missed too many blocks, punish them
	if height > minHeight && signInfo.MissedBlocksCounter > maxMissed {
		modifiedSignInfo = true
		// Downtime confirmed: slash and jail the validator
		// We need to retrieve the stake distribution that signed the block. To do this, we subtract ValidatorUpdateDelay from the evidence height,
		// and subtract an additional 1 since this is the LastCommit.
		// Note that this *can* result in a negative "distributionHeight" of up to -ValidatorUpdateDelay-1,
		// i.e. at the end of the pre-genesis block (none) = at the beginning of the genesis block.
		// This is acceptable since it's only used to filter unbonding delegations & redelegations.
		distributionHeight := height - sdk.ValidatorUpdateDelay - 1

		slashFractionDowntime, err := k.SlashFractionDowntime(ctx)
		if err != nil {
			return err
		}

		coinsBurned, err := k.sk.SlashWithInfractionReason(ctx, consAddr, distributionHeight, power, slashFractionDowntime, st.Infraction_INFRACTION_DOWNTIME)
		if err != nil {
			return err
		}

		if err := k.EventService.EventManager(ctx).EmitKV(
			types.EventTypeSlash,
			event.NewAttribute(types.AttributeKeyAddress, consStr),
			event.NewAttribute(types.AttributeKeyPower, fmt.Sprintf("%d", power)),
			event.NewAttribute(types.AttributeKeyReason, types.AttributeValueMissingSignature),
			event.NewAttribute(types.AttributeKeyJailed, consStr),
			event.NewAttribute(types.AttributeKeyBurnedCoins, coinsBurned.String()),
		); err != nil {
			return err
		}

		err = k.sk.Jail(ctx, consAddr)
		if err != nil {
			return err
		}
		downtimeJailDur, err := k.DowntimeJailDuration(ctx)
		if err != nil {
			return err
		}
		signInfo.JailedUntil = k.HeaderService.HeaderInfo(ctx).Time.Add(downtimeJailDur)

		// We need to reset the counter & bitmap so that the validator won't be
		// immediately slashed for downtime upon re-bonding.
		// We don't set the start height as this will get correctly set
		// once they bond again in the AfterValidatorBonded hook!
		signInfo.MissedBlocksCounter = 0
		err = k.DeleteMissedBlockBitmap(ctx, consAddr)
		if err != nil {
			return err
		}

		k.Logger.Info(
			"slashing and jailing validator due to liveness fault",
			"height", height,
			"validator", consStr,
			"min_height", minHeight,
			"threshold", minSignedPerWindow,
			"slashed", slashFractionDowntime.String(),
			"jailed_until", signInfo.JailedUntil,
		)
	}

	// Set the updated signing info
	if modifiedSignInfo {
		return k.ValidatorSigningInfo.Set(ctx, consAddr, signInfo)
	}
	return nil
}
